Add release automation#266
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new release automation pipeline that prepares a semver release PR, tags the merged release commit, and publishes GitHub Release assets + an npm package + a .unitypackage, with additional guardrails to prevent dev/CI artifacts from entering release payloads.
Changes:
- Added GitHub Actions workflows for Release Prepare (manual), Release Tag (merge-triggered), and Release Publish (tag-triggered).
- Added PowerShell release helper tooling + tests for semver bumping, changelog rotation, and release-notes generation.
- Hardened npm packaging by adding a
fileswhitelist and expanding npm package validation/ignore rules.
Reviewed changes
Copilot reviewed 13 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/validate-npm-package.ps1 | Adds stricter allow/deny-list validation of packed npm contents (top-level entries, scripts, and C# file roots). |
| scripts/unity/export-unitypackage.sh.meta | Unity .meta for the new export script. |
| scripts/unity/export-unitypackage.sh | Adds a Docker-Unity based .unitypackage export pipeline that stages from npm pack. |
| scripts/tests/test-release-tools.ps1.meta | Unity .meta for the new test script. |
| scripts/tests/test-release-tools.ps1 | Adds a lightweight PowerShell test runner for the release helper scripts. |
| scripts/release-tools/write-release-notes.ps1.meta | Unity .meta for the new release-notes writer. |
| scripts/release-tools/write-release-notes.ps1 | Adds a CLI wrapper that writes release notes derived from CHANGELOG.md. |
| scripts/release-tools/release-helpers.ps1.meta | Unity .meta for the new helper module. |
| scripts/release-tools/release-helpers.ps1 | Implements semver parsing/comparison, changelog rotation, and fence-aware changelog section extraction. |
| scripts/release-tools/prepare-release.ps1.meta | Unity .meta for the new prepare script. |
| scripts/release-tools/prepare-release.ps1 | Adds a CLI entrypoint to bump version + rotate changelog (supports dry-run). |
| scripts/release-tools.meta | Adds Unity folder .meta for the new scripts/release-tools directory. |
| package.json | Adds npm files whitelist and wires test:release-tools into validation. |
| docs/project/contributing.md | Updates contributor docs to describe the new release process (replacing release-drafter). |
| .npmignore | Expands ignore rules to exclude more dev/CI/editor artifacts from npm publishes. |
| .markdownlintignore | Ignores PLAN.md and its .meta file for markdown linting. |
| .github/workflows/release.yml | Adds tag-triggered Release Publish pipeline (validate/pack/export/publish). |
| .github/workflows/release-tag.yml | Adds merge-triggered tagging workflow guarded by commit subject + changelog. |
| .github/workflows/release-prepare.yml | Adds manual workflow to prepare and open a release PR (with dry-run option). |
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| ARTIFACTS_ROOT="$(realpath -m "${REPO_ROOT}/.artifacts")" | ||
| PROJECT_DIR="$(realpath -m "${PROJECT_DIR}")" | ||
| if [[ "${PROJECT_DIR}" != "${ARTIFACTS_ROOT}" && "${PROJECT_DIR}" != "${ARTIFACTS_ROOT}/"* ]]; then | ||
| echo "ERROR: Refusing to create the export project outside ${ARTIFACTS_ROOT}: ${PROJECT_DIR}" >&2 | ||
| exit 1 | ||
| fi |
| escaped_version="${package_version//./\\.}" | ||
| if ! grep -Eq "^## \[${escaped_version}\]( - [0-9]{4}-[0-9]{2}-[0-9]{2})?$" CHANGELOG.md; then | ||
| echo "::error::CHANGELOG.md has no exact heading for ${package_version}." | ||
| exit 1 | ||
| fi |
| if ! grep -Eq "^## \[${escaped_version}\]( - [0-9]{4}-[0-9]{2}-[0-9]{2})?$" CHANGELOG.md; then | ||
| echo "::error::Release commit for ${version} has no matching CHANGELOG.md heading." | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 19 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 21 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| throw new InvalidOperationException("Missing -exportOutput argument."); | ||
| } | ||
|
|
||
| Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); |
| throw "Expected exactly one package.json version property for '$CurrentVersion'; found $($matches.Count)." | ||
| } | ||
|
|
||
| $updated = [regex]::Replace($Content, $pattern, "`${1}$NextVersion`${2}", 1) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 21 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| throw new InvalidOperationException("Missing -exportOutput argument."); | ||
| } | ||
|
|
||
| Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); |
| $inFence = $false | ||
| $fenceMarker = '' | ||
|
|
||
| for ($index = 0; $index -lt $Lines.Count; $index++) { | ||
| $line = $Lines[$index] | ||
| $trimmed = $line.TrimStart() | ||
| $isFenceLine = $false | ||
|
|
||
| if ($trimmed -match '^(?<marker>`{3,}|~{3,})') { | ||
| $marker = $Matches['marker'] | ||
| $markerPrefix = $marker.Substring(0, 1) | ||
| if (-not $inFence) { | ||
| $inFence = $true | ||
| $fenceMarker = $markerPrefix | ||
| $isFenceLine = $true | ||
| } elseif ($fenceMarker -eq $markerPrefix) { | ||
| $isFenceLine = $true | ||
| $mask[$index] = $true | ||
| $inFence = $false | ||
| $fenceMarker = '' | ||
| continue | ||
| } | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 21 changed files in this pull request and generated 4 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| "files": [ | ||
| "CHANGELOG.md", | ||
| "CHANGELOG.md.meta", | ||
| "Editor", | ||
| "Editor.meta", | ||
| "LICENSE", | ||
| "LICENSE.meta", | ||
| "README.md", | ||
| "README.md.meta", | ||
| "Runtime", | ||
| "Runtime.meta", | ||
| "Samples~", | ||
| "docs", | ||
| "docs.meta", | ||
| "package.json.meta", | ||
| "scripts/postinstall-hooks.js" | ||
| ], |
| $allowedTopLevelEntries = @( | ||
| 'CHANGELOG.md', | ||
| 'CHANGELOG.md.meta', | ||
| 'Editor', | ||
| 'Editor.meta', | ||
| 'LICENSE', | ||
| 'LICENSE.meta', | ||
| 'README.md', | ||
| 'README.md.meta', | ||
| 'Runtime', | ||
| 'Runtime.meta', | ||
| 'Samples~', | ||
| 'docs', | ||
| 'docs.meta', | ||
| 'package.json', | ||
| 'package.json.meta', | ||
| 'scripts' | ||
| ) |
| } | ||
| } | ||
|
|
||
| $allowedCsRoots = @('Runtime/', 'Editor/', 'Samples~/') |
| for entry in \ | ||
| package.json \ | ||
| package.json.meta \ | ||
| README.md \ | ||
| README.md.meta \ | ||
| LICENSE \ | ||
| LICENSE.meta \ | ||
| CHANGELOG.md \ | ||
| CHANGELOG.md.meta \ | ||
| Runtime \ | ||
| Runtime.meta \ | ||
| Editor \ | ||
| Editor.meta | ||
| do | ||
| copy_package_entry "${entry}" required | ||
| done |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 21 changed files in this pull request and generated 1 comment.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| $scriptsDir = Join-Path $packageDir 'scripts' | ||
| if (Test-Path -LiteralPath $scriptsDir) { | ||
| $allowedScriptsEntries = @('postinstall-hooks.js') | ||
| $scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File | ForEach-Object { | ||
| $_.FullName.Replace("$scriptsDir\", "").Replace("$scriptsDir/", "") -replace '\\', '/' | ||
| } | ||
| foreach ($entry in $scriptEntries) { | ||
| if ($entry -notin $allowedScriptsEntries) { | ||
| $errors += "Unexpected script included in npm package: scripts/$entry" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 1 comment.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| PACKAGE_NAME="$(jq -r '.name' "${REPO_ROOT}/package.json")" | ||
| PACKAGE_VERSION="$(jq -r '.version' "${REPO_ROOT}/package.json")" | ||
| if [[ -z "${OUTPUT_PATH}" ]]; then | ||
| OUTPUT_PATH="${REPO_ROOT}/.artifacts/release/${PACKAGE_NAME}-${PACKAGE_VERSION}.unitypackage" | ||
| fi |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 2 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| push: | ||
| tags: | ||
| # GitHub tag filters are globs, not regex; their filter-pattern cheat | ||
| # sheet defines + as one-or-more of the preceding character. The verify | ||
| # job below still enforces no-leading-zero X.Y.Z semver. | ||
| - "[0-9]+.[0-9]+.[0-9]+" |
| $publishTriggerNarrowlyMatchesReleaseTags = ( | ||
| $publishWorkflowContent.Contains('- "[0-9]+.[0-9]+.[0-9]+"') -and | ||
| -not $publishWorkflowContent.Contains('- "[0-9]*.[0-9]*.[0-9]*"') -and | ||
| $publishWorkflowContent.Contains('Release tags must use unprefixed X.Y.Z semver.') | ||
| ) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 1 comment.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
| 'postinstall-hooks.js', | ||
| 'postinstall-hooks.js.meta' | ||
| ) | ||
| $scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File | ForEach-Object { |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 24 changed files in this pull request and generated 3 comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
Comments suppressed due to low confidence (1)
scripts/validate-npm-package.ps1:300
- Folder meta validation builds $relativePath via string Replace on the absolute path. This can produce incorrect relative paths (and therefore confusing errors) if the extracted path format differs. GetRelativePath is safer and consistent with git-style paths.
# Get relative path for better error messages
$relativePath = $item.FullName.Replace("$packageDir\", "").Replace("$packageDir/", "")
$relativePath = $relativePath -replace '\\', '/'
| Get-ChildItem -LiteralPath $PackageDir -Recurse -File -Force | | ||
| ForEach-Object { | ||
| $_.FullName.Replace("$PackageDir\", "").Replace("$PackageDir/", "") -replace '\\', '/' | ||
| } | | ||
| Sort-Object |
| $scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File -Force | ForEach-Object { | ||
| $_.FullName.Replace("$scriptsDir\", "").Replace("$scriptsDir/", "") -replace '\\', '/' | ||
| } |
| $allowedCsRoots = @('Runtime/', 'Editor/', 'Samples~/', 'Styles/') | ||
| $packedCsFiles = Get-ChildItem -LiteralPath $packageDir -Recurse -File -Filter '*.cs' -Force | ForEach-Object { | ||
| $_.FullName.Replace("$packageDir\", "").Replace("$packageDir/", "") -replace '\\', '/' | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 26 changed files in this pull request and generated no new comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2688c95. Configure here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 26 changed files in this pull request and generated no new comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 27 changed files in this pull request and generated no new comments.
Files not reviewed (6)
- scripts/release-tools.meta: Generated file
- scripts/release-tools/prepare-release.ps1.meta: Generated file
- scripts/release-tools/release-helpers.ps1.meta: Generated file
- scripts/release-tools/write-release-notes.ps1.meta: Generated file
- scripts/tests/test-release-tools.ps1.meta: Generated file
- scripts/unity/export-unitypackage.sh.meta: Generated file

Summary
.unitypackageexport.Validation
npm run validate:prepushnpm run validate:npm-packagebash scripts/unity/export-unitypackage.sh --stage-only --project-dir .artifacts/unity/unitypackage-stage-smokeactionlint .github/workflows/release-prepare.yml .github/workflows/release-tag.yml .github/workflows/release.ymlNote
High Risk
Automates npm and GitHub Release publishing, tag creation, and Unity export using multiple secrets and org build locks; mistakes or partial failures could publish wrong versions or contend with CI Unity seats.
Overview
Replaces ad-hoc release drafting on every main push with a manual Release Prepare → merge → auto-tag → publish pipeline, and tightens what can ship in the npm tarball.
Release Prepare (
workflow_dispatchon the default branch) bumps semver (or accepts an explicitX.Y.Z), runsprepare-release.ps1to rotateCHANGELOG.mdand bumppackage.json/ lockfile, syncs banner/issue templates, runs release and npm validation, then opens arelease/X.Y.ZPR using a GitHub App token so downstream CI runs. Dry-run and failure recovery artifacts are supported.Release Tag runs on default-branch pushes that touch
package.jsonorCHANGELOG.md, recognizesrelease: X.Y.Zsquash-merge commits (with changelog content checks), and pushes an annotated tag via the same app token when absent or already pointing at the commit.Release Publish runs on semver tags: verifies tag ↔
package.json↔ changelog section, validates/packs npm, exports.unitypackageunder the org Unity lock (long job timeout), then publishes npm (skip if version exists) and a GitHub Release with tarball,.unitypackage, checksums, and generated notes.release-drafter is manual-only so it cannot race tag-triggered publishing. Contributing docs describe the new maintainer flow.
Supporting changes: new
scripts/release-tools/*helpers andtest:release-tools; explicitpackage.jsonfilesallowlist; expanded.npmignore; strictervalidate-npm-package.ps1(whole payload vs git, case-sensitive paths, forbidden dev roots); newexport-unitypackage.shstaging fromnpm pack;unity-tests-single-threadednow waits on the main Unity matrix jobs to reduce org-lock contention; contract tests for workflow and package content parity.Reviewed by Cursor Bugbot for commit 5a8e93e. Bugbot is set up for automated code reviews on this repo. Configure here.